Skip to content

Metic 3.0.0: unified TypeScript core + Bun server (#532) - #561

Open
hessius wants to merge 152 commits into
mainfrom
version/3.0.0
Open

Metic 3.0.0: unified TypeScript core + Bun server (#532)#561
hessius wants to merge 152 commits into
mainfrom
version/3.0.0

Conversation

@hessius

@hessius hessius commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Metic 3.0.0 — unified TypeScript core + Bun server

Epic #532. This is the architectural cutover that ends the dual-runtime split: all analysis / profile / curve / recommendation / dial-in / machine-API logic now lives once in a shared @metic/core TypeScript package, consumed by both the browser/native app and a new Bun server. The Python backend is deleted.

Highlights

  • @metic/core (packages/core): single source of truth for the request handler (handle() / tryHandle()), a host-agnostic Platform seam (storage, blob store, machine.fetch, AI provider), and every /api/* route family: profiles CRUD, shot history/analysis, dial-in, annotations, pour-over, recommendations, profile generation, image-proxy, system meta, settings. Backed by a contract-test parity suite.
  • Bun server (apps/bun-server, @metic/server): proxy-mode server + Node Platform (fs-JSON repos, env secrets, TTL AI cache, SQLite backend, timer scheduler). Transparent /api/v1 machine proxy, static SPA, handle() for /api/*, Socket.IO→WS telemetry hub, Tailscale routes over the LocalAPI unix socket.
  • Browser cutover: browser Platform (IndexedDB/Capacitor) implements the same seam; the legacy ~3700-line DirectModeInterceptor is deleted. Native iOS parity (image generation + profile-creation progress) confirmed on-device.
  • Python backend removed (feat!): apps/server gone; unified distroless single-binary image with a CI size gate.
  • Android: hardware back-button support.

Notes

  • VERSION stays 3.0.0-beta.1 (beta channel; latest stays 2.x).
  • All 13 open Dependabot PRs were resolved: 5 Python closed (backend removed), 8 web closed as superseded (3.0.0 already current; react-day-picker removed as dead code).
  • Parity enforced by contract tests; live-verified end-to-end against a physical machine + real Gemini throughout.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

hessius and others added 30 commits July 6, 2026 01:23
Approved brainstorm output: collapse the dual Python/TS runtimes onto a single
shared packages/core handler consumed by both apps/frontend (native + browser)
and a compiled single-binary Bun apps/server on distroless. Drops Python, nginx,
s6, mosquitto, MCP, and MQTT/HA; keeps Tailscale UI via LocalAPI-over-socket.
Seamless upgrade for existing server users (frozen /api/*, /api/ws/live, and
/data contracts; flat-JSON persistence behind a Storage interface).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7-phase plan (monorepo foundation, core extraction, browser platform+host,
node platform+Bun server, tailscale/health/startup, distroless container,
cutover+rollout). Scheduled after 2.7.0. Full TDD step detail for Phase 1;
task-level detail for Phases 2-7 (re-baselined per phase at implementation).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Establishes the architectural keystone for the 3.0.0 unified TS core: a
single Web-standard request handler handle(req, platform) plus the Platform
interface that is the sole seam between portable logic and each host runtime
(browser direct mode, Bun proxy mode). Includes an in-memory mock Platform and
a contract-test harness that locks the frozen /api/* contract host-independently.

Refs #532

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
First Phase 2 extraction slice, proving the extract-and-rewire mechanism
end-to-end: the pure Espresso-Compass logic now lives canonically in
@metic/core (with its test as a core contract test), and the frontend consumes
it via a file: workspace dependency. Verified green: core tests, frontend
vitest, and the full vite build all resolve the linked package.

Refs #532, #528

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Moves the portable, framework-free logic into @metic/core as its canonical
home, each with its test as a core contract test (253 tests total):
analysisSchema, shotFacts, analysisLint, uuid, decentConverter, tags,
profileAnalysis, profileRecommendation. Frontend paths become thin re-export
shims (via the @metic/core file: dependency) so all existing importers keep
working unchanged. This is the substance of former issue #528 done as
extraction toward the unified core.

Verified green: 253 core contract tests + typecheck; 1118 frontend tests
(migrated suites now live in core); full vite build resolves the linked package.

Refs #532, #528

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ic/core

The web-builder stage flattened apps/web into /build and never copied
packages/core, so the frontend's file:../../packages/core dependency could not
resolve under bun install. Copy packages/core into the build context and build
from /build/apps/web, keeping the two-levels-up relative path intact (also fixes
the VERSION path read by vite.config). Verified with a local web-builder build.

Refs #532

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…o @metic/core

Moves the framework-free AI pieces into @metic/core (with their tests as core
contract tests): AIServiceError/AIErrorCode (aiErrors), the analysis domain
knowledge + fact-sheet builder (analysisKnowledge), and the shared prompt
builders (prompts). Frontend paths remain thin re-export shims. The i18n- and
SDK-coupled provider/retry/model-resolver layers stay in the frontend for the
later provider abstraction (Phase 2.6).

Verified green: 298 core contract tests + typecheck; 1073 frontend tests; full
vite build resolves the linked package; lint clean.

Refs #532, #528

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds apps/bun-server, the 3.0.0 single-binary Bun server that consumes
@metic/core. Verified end-to-end against a live Meticulous machine.

- platform/node.ts: filesystem-backed Node implementation of the core
  Platform interface (JSON repos over the existing /data shapes, env
  secrets, TTL aiCache, blob store, timer scheduler).
- machineProxy.ts: transparent reverse proxy for /api/v1/* to the machine
  (streams body, mirrors status/headers, strips hop-by-hop).
- static.ts: SPA static serving with immutable asset caching + index
  fallback and path-traversal protection.
- telemetryHub.ts: single upstream Socket.IO connection fanned out to
  browser WebSocket clients at /api/ws/live, emitting the exact flat
  snapshot shape (+ _ts, _heartbeat) the frontend already consumes;
  mapping mirrors direct-mode useMachineTelemetry for runtime parity.
- server.ts / main.ts: Bun.serve wiring (WS upgrade, /api/v1 proxy,
  /api/* -> handle(), static) and serve|healthcheck subcommands.

Additive only: Python apps/server and DirectModeInterceptor remain the
live runtimes until the Phase 7 cutover. 19 tests pass; typecheck clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make the package's own `bun run typecheck` gate clean: explicit generic
on aiCache.get and unknown-cast fetch mocks. No runtime behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the shot-annotations CRUD route family into @metic/core against the
Platform storage seam, matching Python (shot_annotations_service.py) and
native (directModeStorage.ts) behavior exactly.

- handleAnnotationRoutes dispatcher: GET/PATCH/DELETE single annotation and
  GET summaries, keyed by `${date}/${filename}`.
- PATCH parity: rating-only merge keeps existing text; clearing both deletes.
- validateRating: integer 1-5, else 422; invalid JSON -> 400.
- Node Platform: add fsKeyedMapRepo backing annotations with a single
  shot_annotations.json object map (Python data-volume compatible, handles
  slash/colon-containing ids that the dir-based fsRepo would collide).
- 14 core contract tests + 1 node-platform repo test. Live-verified end-to-end
  through the Bun server against machine 192.168.50.168.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the Dial-In Guide session routes into @metic/core against the Platform
storage seam, matching Python (dialin_service.py + dialin.py) and native
(directModeStorage.ts + DirectModeInterceptor.ts dispatch) exactly.

- handleDialInRoutes: create/get/list(+status filter)/delete sessions, add
  iterations (auto-incrementing, active-only), update iteration
  recommendations, rule-based /recommend, and /complete. Served under both
  /api/dialin/... and the bare /dialin/... alias.
- Validation mirrors the native runtime (coffee roast/process enums, taste
  x/y in [-1,1], recommendation string lists) and returns FastAPI-style
  { detail } errors with matching 400/404 status.
- Rule-based recommendations reproduce the exact taste-coordinate guidance and
  strings shared by both runtimes; the Python AI path layers on later with the
  provider abstraction (native /recommend is already rules-only).
- Node Platform: back dialInSessions with fsKeyedMapRepo -> dialin_sessions.json
  (id-keyed map matching Python's on-disk layout for Phase 7 migration), same
  approach as annotations.
- 20 core contract tests. Live-verified end-to-end through the Bun server
  against machine 192.168.50.168.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the pour-over preferences routes into @metic/core against the Platform
storage seam, matching Python (pour_over_preferences.py + pour_over.py) and
native (directModeStorage.ts + DirectModeInterceptor.ts dispatch).

- handlePourOverRoutes: GET returns stored per-mode preferences (defaults when
  unset), PUT normalizes, persists, and echoes the normalized result.
- Validation mirrors the native runtime's strict typing (wrong types throw)
  and its ratio-mode defaults of dose 18g / ratio 15, a deliberate divergence
  from the Python nulls that this unification adopts since the native/browser
  runtime is what the frontend relies on. Unknown keys are dropped.
- Error shapes match native: 400 { detail: "Invalid preferences" } for bad
  input or invalid JSON, 500 for load/save failures.
- Node Platform: rename the singleton file to pour_over_preferences.json to
  match Python's on-disk layout for Phase 7 migration.
- 6 core contract tests. Live-verified end-to-end through the Bun server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a PlatformAI seam (isConfigured/generateText/optional generateImage)
to the core Platform interface, mirroring the frontend AIProvider so the
browser Platform can delegate to the active provider in Phase 3.

Wire a Gemini-backed PlatformAI into the Node platform via @google/genai,
reading GEMINI_API_KEY/GEMINI_MODEL from the environment.

Port the dial-in recommendation prompt builder to core and make the
dial-in /recommend route AI-first: build the prompt, call the provider,
strip markdown fences, parse JSON, cap at 6 recommendations (source "ai");
on any error or when unconfigured, fall back to the rule-based path
(source "rules"). This matches the Python oracle and gives the native
runtime AI dial-in recommendations once wired in Phase 3.

Add scriptedAI/throwingAI/unconfiguredAI mock helpers and contract tests
for the AI path plus a prompt-builder unit test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the Platform's machine seam with a fetch(path, init?) method so core
routes can read machine state (shot history, profiles) uniformly across hosts.
The Node platform joins relative paths onto the resolved base URL and fetches
server-side; the browser platform will fetch over the LAN in Phase 3.

Add scriptedMachine/unwiredMachine mock helpers for contract tests and cover
the Node URL-join + unconfigured-rejection behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the shot-analysis building blocks from the native DirectModeInterceptor
into @metic/core so both runtimes share one implementation:
- computeRichLocalAnalysis (structured local shot analysis) + its helpers
- parseRecommendationsJson / isActionableRecommendation /
  isRecommendationPatchable / termMatches (RECOMMENDATIONS_JSON extraction)
- buildAnalyzeLlmPrompt and the PROFILING_KNOWLEDGE constant it needs

The ports are byte-identical (prompts) or logic-identical (analysis) to the
native source, reusing the already-ported buildShotFacts. Add contract tests
for the analysis output shape and the recommendation parsing/patchability
rules. These modules back the shot-analysis routes ported next.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the /api/shots/analyze, /api/shots/analyze-llm and
/api/shots/analyze-recommendations routes to @metic/core, served by handle()
for both runtimes (also under the bare /shots/... alias):
- analyze: fetch the shot from the machine and return the structured local
  analysis (computeRichLocalAnalysis).
- analyze-llm: build the analysis prompt over the shot data, run it through
  the AI provider seam with the existing validate/retry/repair loop, and cache
  the result for recommendation extraction.
- analyze-recommendations: parse the RECOMMENDATIONS_JSON block (from the
  posted text or the cache) and flag patchability against the profile's
  variables fetched from the machine.

Reuses the ported shot-analysis logic, prompt builder and recommendation
parser. Adds 9 contract tests driven by the scriptedMachine/scriptedAI mocks.
Verified live through the Bun server against a real machine and Gemini:
structured analysis, a full AI analysis, and cached recommendation extraction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add /api/profiles/recommend and /api/profiles/find-similar to @metic/core
(also under the bare /profiles/... alias), reusing the shared
profileRecommendation scorer. Profiles are read from the machine with
?full=true so stages are included for structural scoring in a single request,
matching the server's canonical behavior and improving the native runtime
(which previously scored off stage-less list data).

Adds 5 contract tests asserting parity with the shared scorer. Verified live
through the Bun server against a real machine catalogue.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add POST /api/profile/{name}/apply-recommendations to @metic/core (also under
the bare /profile/{name}/... alias). It patches selected recommendations onto a
machine profile and saves it back:
  - global temperature / final_weight,
  - adjustable profile variables (info-only / non-adjustable are skipped),
  - stage exit_triggers and limits,
  - a fuzzy fallback that recovers model-invented positional ids
    (e.g. "pressure_2") by type + current_value + stage.

Client-only metadata (change_id, in_history, has_description) is stripped
before saving, matching the native runtime. The server's defensive bounds
guards (temperature <= 100 C, final_weight > 0) are kept so invalid values are
skipped rather than written to the machine. Adds 12 contract tests.

Also fixes a latent DOM-lib typing issue in routes/profiles.ts (toNumber took
FormDataEntryValue, which is unavailable under the bun-server tsconfig) and
exports the MockMachine test type. Verified live through the Bun server against
a real machine (idempotent apply round-trips find -> get -> patch -> save).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add POST /api/profile/{id}/regenerate-description to @metic/core (also under
the bare /profile/{id}/... alias). It regenerates a profile's human-readable
description, preferring an AI write-up and falling back to a deterministic
static summary:
  - resolve the profile from the machine (identifier as profile id, then name),
  - when AI is configured, build the barista prompt (with the shared tags
    prompt), generate, strip the Tags line, resolve any leftover variable
    placeholders to concrete values, cache the description and parsed tags,
  - otherwise (or on AI failure / a "generated without AI" signal) build and
    cache the static description.

The two parity oracles resolve the profile from runtime-specific caches
(server: profile-generation history; native: machine shot history + in-memory
profile caches) that have no host-independent equivalent, so this port resolves
through the machine client, the one path available on every host. The browser
Platform will map the new description / ai-tags storage repos onto its existing
native caches in Phase 3.

Adds two new Platform storage repos (descriptions, aiTags) wired in the Node
platform (profile_descriptions.json, profile_ai_tags.json) and the mock. Ports
buildStaticProfileDescription + resolveDescriptionPlaceholders into
logic/profileDescription.ts (reusing the existing core tags helpers). Adds 13
contract tests. Verified live through the Bun server against a real machine +
real Gemini (AI description by id and by name, placeholder-free, tags stripped,
cached to disk, 404 path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…he core package

Ports the flagship profile-generation route to @metic/core against the
Platform seam: builds the full profiling prompt (+optional image), runs the
AI generate/validate/retry loop, converts the Gemini JSON to OEPF, and writes
the new profile to the machine, caching the analysis text as its description.

Adds profileValidator, oepf, and the prompt/validate/retry helpers, all ported
byte/logic-identical from the native oracle.

Fixes a latent parity bug in the OEPF converter (both runtimes): object-form
dynamics whose points were already [t, v] arrays skipped $ref/number
resolution, so arithmetic expression strings (e.g. "10000 * ($ratio / 2.5)")
survived and the machine rejected the save with "is not referencing a variable
but is a string". Now array-form points are resolved like the other two
dynamics branches. Live-verified end-to-end against machine 192.168.50.168 +
real Gemini: profile generated, saved to the machine, then cleaned up.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ystone)

Adds apps/web/src/services/platform/browserPlatform.ts: the direct-mode
counterpart to the Bun server's Node platform. It implements the @metic/core
Platform interface with browser primitives so the SAME handle(request,
platform) can run entirely client-side, the foundation for replacing the
bespoke per-route logic in DirectModeInterceptor.

- Storage: IndexedDB-backed repos that persist documents VERBATIM under the
  settings key-value store, faithfully mirroring the Node fsKeyedMapRepo /
  fsSingletonRepo semantics (so the shared contract tests hold on both hosts).
  Blob store maps to the profile-image IDB store (Blob <-> Uint8Array).
- Machine: fetch joins core's path onto getDefaultMachineUrl(); takes an
  injectable original fetch to avoid re-entrancy with the fetch interceptor.
- AI: delegates to the frontend's active provider (identical generateText
  signature); image output converted Blob -> bytes. No scheduler (direct mode
  cannot run deferred actuation, so schedule routes 501, matching the legacy
  interceptor).

Includes an integration test that runs @metic/core handle() against the real
IndexedDB-backed platform (fake-indexeddb + fake machine fetch + scripted
provider) across annotations, dial-in, pour-over, machine-fetch, and AI seams.
7 tests. Not yet wired into window.fetch: the live cutover and
DirectModeInterceptor deletion are gated on on-device parity verification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ore package

Adds the system/meta route family to @metic/core so the Bun server (proxy
mode) serves GET/POST /api/settings, GET /api/network-ip, and GET /api/version
that the frontend calls at startup. Previously only the native
DirectModeInterceptor implemented these; the server returned 404, leaving
settings and version unavailable in proxy mode.

- packages/core/src/routes/system.ts: settings payload mirrors the stable
  frontend contract (geminiApiKeyConfigured/meticulousIp/authorName/
  geminiModel/mqttEnabled); the raw AI key is never returned. Configured state
  uses platform.ai.isConfigured() (the canonical cross-host signal).
- platform.ts: add optional Platform.appVersion for GET /api/version.
- node platform: resolveAppVersion() (APP_VERSION env, else VERSION file);
  browser platform: appVersion from the __APP_VERSION__ build define.
- 9 contract tests; live-verified against the Bun server + machine
  192.168.50.168 (settings round-trip, version, network-ip) and via a headless
  browser load of the built proxy-mode frontend (startup 404s cleared).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rlay

Adds the /api/history family to @metic/core using native semantics
(user-approved): the machine shot history from /api/v1/history filtered
by local soft-delete tombstones, with per-shot notes and AI descriptions
overlaid from the Platform storage. Includes list, single detail with
profile_json, notes GET/PATCH round-trip, and DELETE tombstone.

Fixes a latent default-parameter bug where an absent limit query param
resolved to 0 (Number(null)) instead of the intended fallback of 50.

Live-verified through the Bun server against machine 192.168.50.168:
20 shots normalized, notes round-trip, tombstone delete 20 to 19.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the GET read family to @metic/core with native semantics, all
served through the shared visible-history loader so local soft-deletes
apply consistently:
  - /api/last-shot
  - /api/shots/dates
  - /api/shots/recent
  - /api/shots/recent/by-profile
  - /api/shots/by-profile/{name}
  - /api/shots/data/{date}/{filename}

Extracts the machine-history store access (loadVisibleHistory, overlay,
notes, descriptions) into logic/historyStore so the history and shots
families share one implementation, and adds hasRecentShotAnnotation to
machineHistory. Annotation flags reuse the ported annotations summaries.

Live-verified through the Bun server against machine 192.168.50.168:
20 recent shots, 4 profile groups, paged by-profile, and 338-point
telemetry converted for /api/shots/data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the machine actuation and status family to @metic/core with native
semantics (mirrors DirectModeInterceptor + commands.py/system.py):
  - POST /api/machine/command/start | stop | load-profile
  - POST /api/machine/preheat
  - GET  /api/machine/status/health   (watcher /status on port 3000)
  - GET  /api/machine/system-info
  - GET  /api/machine/status          (synthetic idle)
  - GET  /api/machine/detect          (501)
  - POST /api/machine/schedule-shot   (501, no scheduler)

Ports the pure watcher-response transform + size/uptime parsers into
logic/watcher, reached through the machine seam which already passes
absolute URLs through on both platforms.

Live-verified through the Bun server against machine 192.168.50.168:
status/health returns 7 live services + system metrics, status idle,
detect 501; system-info degrades to null per key exactly as native does
against this firmware. Actuation paths covered by contract tests to
avoid dispensing on the live machine.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the machine profile CRUD + actuation family (run-profile,
run-with-overrides, import, import-all, convert-decent, import-from-url,
order, load, rename, delete, machine/profiles list, profile/{id}/json,
orphaned, target-curves, edit, profile/{name} GET, sync/status, sync)
into @metic/core against the Platform seam, mirroring the native
DirectModeInterceptor oracle. Adds logic/targetCurves.ts and
logic/profileList.ts. 34 contract tests; live-verified against machine
192.168.50.168 (36 profiles, stages, target-curves, rename + edit
round-trips).

Fix a latent parity bug in BOTH runtimes: /target-curves resolved the
profile from the machine profile list, which omits stages, so it
returned an empty curve set for saved (non-override) profiles. Both
runtimes now fetch the full profile by id when the list entry lacks
stages, matching the include_stages fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port modelResolver (dynamic Gemini model discovery + ranking) into
@metic/core as the single source of truth; the native
services/ai/modelResolver becomes a thin re-export shim. Add optional
PlatformAI.listModels()/currentModel() seam, implemented by the Node
platform (Gemini SDK models.list + core ranking) and the browser
platform (active provider). Add system routes /api/available-models
(live discovery, static fallback, text-only filtering), /api/changelog
(GitHub releases), /api/update-method, /api/tailscale-status, and 501
stubs for check-updates/restart/beta-channel/feedback. 8 new contract
tests (445 core total). Live-verified against machine 192.168.50.168 +
real Gemini: 18 text models (no image/tts/embedding leakage), 5 real
releases, stubs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the bundled pour-over recipe library into
packages/core/src/data/recipes.ts as the single source of truth and add
routes/recipes.ts serving GET /api/recipes (sorted by name) and
GET /api/recipes/{slug} (404 on unknown). The native DirectModeInterceptor
now imports POUR_OVER_RECIPES from core instead of carrying its own inline
copy (removes ~11KB of duplicated data). Un-ignore packages/core/src/data
so the source asset is tracked. 3 contract tests (448 core total); native
interceptor suite still green (55). Live-verified against the Bun server:
9 recipes sorted, single-slug lookup, 404 path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The built config.json ships a dev serverUrl (http://localhost:8000) that
points at a dead host in production. The legacy nginx deployment rewrote
it to an empty string so the SPA talks to its own origin over relative
URLs. The Bun server now does the same: GET /config.json returns
{"serverUrl":""} regardless of the build artifact. Adds a booted-server
test and live-verified through the Bun server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
hessius and others added 30 commits August 22, 2026 11:41
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… v2)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tings store

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The controller previously ended the activity with nil content, leaving the
last mid-extraction frame on screen and never producing the terminal summary
(final weight/time/avg temp). Track the last extraction weight/elapsed and
build a .done ContentState via ShotContentBuilder.summary on stop().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native direct-mode connection used the raw stored machine URL and never
applied the existing :8080 -> :80 port fallback (or rediscovery) once past
onboarding. When firmware moves the API from :8080 to :80, the app was stuck
retrying the dead port forever (TCP RST / ECONNREFUSED) even though the machine
was reachable — the official app followed the mDNS-advertised port and worked.

- useResolvedMachineUrl: on mount, probe the stored URL's port candidates and
  adopt+persist the reachable one (cheap, bounded — no mDNS on launch).
- MachineServiceProvider: if the adapter never connects within 8s, run full
  discovery once and adopt the machine's current address (DHCP IP-change
  recovery), persisting it so the adapter rebuilds against the healed URL.
- discovery: add resolveAndHealMachineUrl (port fallback -> rediscovery), tested.

Mirrors the server runtime's lazy self-heal (platform/node.ts) for parity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename the settings section 'Home-screen widgets' to 'Widgets' across all 6 locales.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Favourites persist a fully-resolved absolute profile-image URL that pins the
machine address+port at favourite time (e.g. http://IP:8080/...). When the
machine's API port changes (firmware :8080 -> :80) or its IP is reassigned,
that URL becomes unreachable: the native cacheImage download fails, no file is
written to the App Group, and widget tiles silently fall back to a monogram.
Re-adding the widget can't fix it because the app never re-resolved the URL.

- Add rehostMachineImageUrl(): rewrites the origin of machine profile-image
  URLs (path /api/v1/profile/image/) onto the current base, leaving data URIs
  and external CDN URLs untouched.
- useWidgetSync: re-host every favourite image before pushing to the bridge,
  and re-push favourites on MACHINE_URL_CHANGED so cached images refresh the
  moment the machine URL self-heals.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Four defects surfaced on-device:

1. Triggered only when entering the Live View. useLiveActivitySync was mounted
   inside LiveShotView, so it ran only while that screen was open. Hoist it to
   the app root (alongside useShotTelemetryRecorder) so the activity starts the
   moment a shot is detected/started, regardless of the current view.

2. Heating temperature showed 0°. The machine emits heating thermocouples on
   the Socket.IO 'sensors' event, but ShotStreamer only recognised
   'temperatures' (which never fires), so chamber/head stayed nil -> 0°. Accept
   'sensors' (keep 'temperatures' for older firmware) and, since heating has no
   'status' frames carrying temps, push a Live Activity update on each temp
   frame (merged into the last known frame).

3. Chamber/head were swapped vs. the in-app live view. Match the app mapping:
   t_bar_up = boiler / 'Brew Chamber', t_bar_down = 'Brew Head'.

4. Expanded Dynamic Island overflowed (long profile name in the .leading region
   spilled past the camera) and showed almost no content. Keep leading/trailing
   compact (icon + glanceable) and render the full ShotLockScreenView in the
   .bottom region.

Swift + web tests updated; MeticWidgetsTests 43 pass, web suite 1227 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The expanded Dynamic Island clipped a few pixels on every edge: the phase icon
(top-left), the glanceable degree value (top-right) and the heating progress
bars (both ends) sat flush against the rounded corners. Inset the leading /
trailing region content and add horizontal padding to the bottom region so
nothing touches the corners. The lock-screen presentation was already padded
and unaffected.

Also replace the bare orange header dot with the shared 'Metic.' wordmark so
the activity is recognisably branded (the lone dot was ambiguous).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Live Activity froze mid-heating and never recovered because ShotStreamer
finished its stream permanently on the first socket drop (a transient WiFi
blip or a missed Engine.IO heartbeat killed it for good), and every update
passed staleDate: nil so the frozen values looked live.

- ShotStreamer now reconnects with exponential backoff (0.5s -> 8s), resetting
  on a successful handshake, capped by a max-duration guard so it can't spin
  forever. Both the design spec (reconnect/backoff, line 126/168) called for
  this; it was missing from the implementation.
- Every activity.update now carries a staleDate (~12s) so when iOS suspends the
  backgrounded app and the local socket stops, the system dims the activity
  instead of presenting stale values as current.
- start() ends any activity orphaned by a prior app termination so a new shot
  can't stack a duplicate.

Note: continuous background updates while the app is suspended still require an
APNs push relay, which native mode intentionally lacks (accepted constraint).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…trings

When iOS suspends the backgrounded app the local socket can't run, so the
activity goes stale. It now shows a localised 'Open Metic to resume live
updates' hint (driven by ActivityViewContext.isStale) so the user knows the
frozen values aren't live and how to refresh them — the app can't self-reconnect
while suspended (no code runs; only APNs push, which native mode lacks, could).

Also fixes an i18n gap: every widget label (Brew Chamber/Head, Ready, Start,
tiles, summary, Done) was hardcoded English. They're now passed from the web
layer via a ShotLocalizedStrings bundle (single source of truth = react-i18next),
localised across all 6 locales, with English defaults as a safety fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update all user-facing and contributor docs to reflect the 3.0.0 single
distroless Bun binary architecture and the removal of the legacy Python/
FastAPI + s6-overlay + nginx + MQTT/bridge/MCP stack.

- README/API: drop s6-svc, /api/logs, /api/bridge, /docs, missing addons.ps1;
  correct the WebSocket + machine-command docs (Socket.IO, not MQTT)
- HOME_ASSISTANT: convert to a clean "removed in 3.0.0" + migration doc
- UPDATING/IOS_SHORTCUTS: fix stale s6/8000//docs references
- docs/local-shot-analysis: point at shared @metic/core TS logic + vitest
- AGENTS/CLAUDE/GEMINI + copilot-instructions + CONVENTIONS + skills: Bun +
  @metic/core stack; correct the dual-runtime guardrail (logic now shared in
  @metic/core, DirectModeInterceptor removed)
- apps/web + android READMEs: fix stale proxy/DirectModeInterceptor claims

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clean up development-only files from the published GitHub repo. Local copies
of the design docs are retained via .gitignore; they are simply no longer
tracked/published.

- Untrack docs/superpowers/ plans & specs (shipped-feature design history)
- Untrack the accidentally committed .copilot/ session state
- Remove the transient .release-notes-beta.md
- gitignore docs/superpowers/, .copilot/, and .release-notes-beta.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants